<?php
/**
 * Aero - Xistins Technologies PHP Framework
 * 
 * LICENSE
 * This source file is subject to the New BSD license that is bundled 
 * with this package in the LICENSE.txt. It is also available through 
 * the world-wide-web at this URL: http://dev.xistins.com/license/production
 * If you did not receive a copy of this license and are unable to obtain
 * it through the world-wide-web, please send an email to 
 * license@xistins.com so we can send you a copy immediately.
 * 
 * @category   Aero
 * @package    Aero_Controller
 * @subpackage Application
 * @copyright  http://dev.xistins.com/license/production     New BSD
 * @version    $Id: Application.php 4 2009-11-14 18:35:45Z csouth@xistins.com $
 */

/** @see Aero_Loader **/
require_once 'Aero/Loader.php';

/** @see Aero_View **/
require_once 'Aero/View.php';

/**
 * Application Controller
 * 
 * @category   Aero
 * @package    Aero_Controller
 * @subpackage Application
 * @copyright  http://dev.xistins.com/license/production     New BSD
 * @version    $Id: Application.php 4 2009-11-14 18:35:45Z csouth@xistins.com $
 */
class Aero_Controller_Application
{

    /**
     * Base URL
     * @var string
     */
    protected $_baseUrl;

    /**
     * Default controller directory name
     * @var string
     */
    protected $_controllerDirectory = 'controllers';

    /**
     * Default action
     * @var string
     */
    protected $_defaultAction = 'index';

    /**
     * Default controller
     * @var string
     */
    protected $_defaultController = 'index';

    /**
     * Default module
     * @var string
     */
    protected $_defaultModule = 'default';

    /**
     * Singleton instance
     * 
     * Marked as protected to allow extension of the class. To extend,
     * simply override {@link getInstance()}.
     * 
     * @var Aero_Controller_Application
     */
    protected static $_instance = null;

    /**
     * Directory path on the hard drive that contains application modules
     * This is in relation to the bootstrap file(I.E. index.php), or a 
     * static path.
     * @var string
     */
    protected $_moduleDirectory = '../application/modules';

    /**
     * Params from Request
     * @TODO Need getter and setter methods
     * @var array
     */
    protected $_params = array();

    /**
     * Requested Actions
     * @var string
     */
    protected $_requestedAction;

    /**
     * Requested Controller
     * @var string
     */
    protected $_requestedController;

    /**
     * Requested Module
     * @var string
     */
    protected $_requestedModule;

    /**
     * Request URI
     * @var string
     */
    protected $_requestUri;

    /**
     * Routes
     * @var array
     */
    protected $_routes;

    /**
     * View
     * @var Aero_View
     */
    protected $_view;

    /**
     * Constructor
     *
     * Instantiate using {@link getInstance()}; application is a singleton
     * object.
     * 
     * @return void
     */
    protected function __construct()
    {
        $this->setBaseUrl()->_setRequestUri();
    }

    /**
     * Enforce singleton; disallow cloning
     *
     * @return void
     */
    private function __clone()
    {}

    /**
     * Singleton instance
     *
     * @return Aero_Controller_Application
     */
    public static function getInstance()
    {
        if (null === self::$_instance) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    /**
     * Set base url.
     * 
     * @param  string $baseUrl
     * @return Aero_Controller_Application
     * @throws Aero_Controller_Exception for non-string $baseUrl
     */
    public function setBaseUrl($baseUrl = null)
    {
        if (null === $baseUrl) {
            // Attempt to 'sense' the base url.
            $this->_baseUrl = substr($_SERVER['PHP_SELF'], 0, - strlen(array_pop(array_values(explode('/', $_SERVER['PHP_SELF'])))));
            return $this;
        }
        /**
         * $baseUrl must be string
         */
        if (! is_string($baseUrl)) {
            $type = ucfirst(gettype($baseUrl));
            Aero_Loader::loadClass('Aero_Controller_Exception');
            throw new Aero_Controller_Exception("Parameter provided to Aero_Controller_Application::setBaseUrl()
    		                                     must be a string. $type was provided.");
        }
        $this->_baseUrl = $baseUrl;
        return $this;
    }

    /**
     * Get base url.
     * 
     * @return string
     */
    public function getBaseUrl()
    {
        return $this->_baseUrl;
    }

    /**
     * Set application module controller directory name
     * 
     * @param  string $directoryName
     * @return Aero_Controller_Application
     * @throws Aero_Controller_Exception for non-string $directoryName
     */
    public function setControllerDirectory($directoryName)
    {
        if (! is_string($directoryName)) {
            $type = ucfirst(gettype($directoryName));
            Aero_Loader::loadClass('Aero_Controller_Exception');
            throw new Aero_Controller_Exception("Parameter provided to 
                                                 Aero_Controller_Application::setDefaultControllerDirectory() 
                                                 must be a string. $type was provided.");
        }
        $this->_controllerDirectory = $directoryName;
        return $this;
    }

    /**
     * Get application module directory
     * 
     * @return string
     */
    public function getControllerDirectory()
    {
        return $this->_controllerDirectory;
    }

    /**
     * Set default action
     * 
     * @param  string $action
     * @return Aero_Controller_Application
     * @throws Aero_Controller_Exception for non-string $action
     */
    public function setDefaultAction($action)
    {
        /**
         * $action must be string
         */
        if (! is_string($action)) {
            $type = ucfirst(gettype($action));
            Aero_Loader::loadClass('Aero_Controller_Exception');
            throw new Aero_Controller_Exception("Parameter provided to 
                                                 Aero_Controller_Application::setDefaultAction() must
                                                 be a string. $type was provided.");
        }
        $this->_defaultAction = $action;
        return $this;
    }

    /**
     * Get default action
     * 
     * @return string
     */
    public function getDefaultAction()
    {
        return $this->_defaultAction;
    }

    /**
     * Set default controller
     * 
     * @param  string $controller
     * @return Aero_Controller_Application
     * @throws Aero_Controller_Exception for non-string $controller
     */
    public function setDefaultController($controller)
    {
        /**
         * $controller must be string
         */
        if (! is_string($controller)) {
            $type = ucfirst(gettype($controller));
            Aero_Loader::loadClass('Aero_Controller_Exception');
            throw new Aero_Controller_Exception("Parameter provided to 
                                                 Aero_Controller_Application::setDefaultController() must
                                                 be a string. $type was provided.");
        }
        $this->_defaultAction = $controller;
        return $this;
    }

    /**
     * Get default controller
     * 
     * @return string
     */
    public function getDefaultController()
    {
        return $this->_defaultController;
    }

    /**
     * Set default module
     * 
     * @param  string $module
     * @return Aero_Controller_Application
     * @throws Aero_Controller_Exception for non-string $module
     */
    public function setDefaultModule($module)
    {
        /**
         * $module must be string
         */
        if (! is_string($module)) {
            $type = ucfirst(gettype($module));
            Aero_Loader::loadClass('Aero_Controller_Exception');
            throw new Aero_Controller_Exception("Parameter provided to 
                                                 Aero_Controller_Application::setDefaultModule() must
                                                 be a string. $type was provided.");
        }
        $this->_defaultModule = $module;
        return $this;
    }

    /**
     * Get default controller
     * 
     * @return string
     */
    public function getDefaultModule()
    {
        return $this->_defaultModule;
    }

    /**
     * Set application module directory
     * 
     * @param  string $module
     * @return Aero_Controller_Application
     * @throws Aero_Controller_Exception for non-string $directory or non-existant directory
     */
    public function setModuleDirectory($directory)
    {
        if (! is_string($directory)) {
            $type = ucfirst(gettype($directory));
            Aero_Loader::loadClass('Aero_Controller_Exception');
            throw new Aero_Controller_Exception("Parameter provided to 
                                                 Aero_Controller_Application::setModuleDirectory() must
                                                 be a string. $type was provided.");
        } elseif (! file_exists($directory) || ! is_readable($directory)) {
            Aero_Loader::loadClass('Aero_Controller_Exception');
            throw new Aero_Controller_Exception("Directory provided to 
                                                 Aero_Controller_Application::setModuleDirectory() must
                                                 exist and be readable by web server.");
        }
        $this->_defaultModuleDirectory = $directory;
        return $this;
    }

    /**
     * Get application module directory
     * 
     * @return string
     */
    public function getModuleDirectory()
    {
        return $this->_moduleDirectory;
    }

    /**
     * Set requested action
     * 
     * @param  string $action
     * @return Aero_Controller_Application
     * @throws Aero_Controller_Exception for non-string $action
     */
    public function setRequestedAction($action)
    {
        if (! is_string($action)) {
            $type = ucfirst(gettype($action));
            Aero_Loader::loadClass('Aero_Controller_Exception');
            throw new Aero_Controller_Exception("Parameter provided to 
                                                 Aero_Controller_Application::setRequestedAction() must
                                                 be a string. $type was provided.");
        }
        $this->_requestedAction = $action;
    }

    /**
     * Get requested action
     * 
     * @return string
     */
    public function getRequestedAction()
    {
        if (is_null($this->_requestedAction)) {
            return $this->_defaultAction;
        }
        return $this->_requestedAction;
    }

    /**
     * Set requested controller
     * 
     * @param  string $controller
     * @return Aero_Controller_Application
     * @throws Aero_Controller_Exception for non-string $controller
     */
    public function setRequestedController($controller)
    {
        if (! is_string($controller)) {
            $type = ucfirst(gettype($controller));
            Aero_Loader::loadClass('Aero_Controller_Exception');
            throw new Aero_Controller_Exception("Parameter provided to 
                                                 Aero_Controller_Application::setRequestedController() must
                                                 be a string. $type was provided.");
        }
        $this->_requestedController = $controller;
    }

    /**
     * Get requested controller
     * 
     * @return string
     */
    public function getRequestedController()
    {
        if (is_null($this->_requestedController)) {
            return $this->_defaultController;
        }
        return $this->_requestedController;
    }

    /**
     * Set requested action
     * 
     * @param  string $action
     * @return Aero_Controller_Application
     * @throws Aero_Controller_Exception for non-string $action
     */
    public function setRequestedModule($module)
    {
        if (! is_string($module)) {
            $type = ucfirst(gettype($module));
            Aero_Loader::loadClass('Aero_Controller_Exception');
            throw new Aero_Controller_Exception("Parameter provided to 
                                                 Aero_Controller_Application::setRequestedModule() must
                                                 be a string. $type was provided.");
        }
        $this->_requestedModule = $module;
    }

    /**
     * Get requested action
     * 
     * @return string
     */
    public function getRequestedModule()
    {
        if (is_null($this->_requestedModule)) {
            return $this->_defaultModule;
        }
        return $this->_requestedModule;
    }

    /**
     * Set application View
     * 
     * @param  Aero_View $view
     * @return Aero_Controller_Application
     */
    public function setView(Aero_View $view)
    {
        $this->_view = $view;
        return $this;
    }

    /**
     * Get application view
     * 
     * @return Aero_View
     */
    public function getView()
    {
        if(null === $this->_view) {
            Aero_Loader::loadClass('Aero_View');
            $this->_view = new Aero_View();
        }

        return $this->_view;
    }

    /**
     * Set request param
     * 
     * @param  string $name
     * @param  mixed $value
     * @return Aero_Controller_Application
     */
    public function setParam($name, $value)
    {
        $this->_params[$name] = $value;
        return $this;
    }

    /**
     * Set request params
     * 
     * @param  array $params
     * @return Aero_Controller_Application
     */
    public function setParams($params)
    {
        array_merge($params, $this->_params);
        return $this;
    }

    /**
     * Get request param
     * 
     * @param  string $name
     * @return mixed
     */
    public function getParam($name)
    {
        if(isset($this->_params[$name])) {
            return $this->_params[$name];
        }

        return false;
    }

    /**
     * Get request params
     * 
     * @return array
     */
    public function getParams()
    {
        return $this->_params;
    }

    /**
     * Register Route
     * 
     * @param  string $name
     * @param  array  $routeInfo
     * @return Aero_Controller_Application
     */
    public function registerRoute($name, array $routeInfo)
    {
        $routeInfo['uri'] = ltrim($routeInfo['uri'], '/');
        $this->_routes[$name] = $routeInfo;
        return $this;
    }

    /**
     * Convience Method; Register an array of routes at once
     * 
     * @param  array $routes
     * @return Aero_Controller_Application
     */
    public function registerRoutes(array $routes)
    {
        foreach ($routes as $name => $routeInfo) {
            $this->registerRoute($name, $routeInfo);
        }
        return $this;
    }

    /**
     * Sort routes by slash count desc
     * 
     * @return Aero_Controller_Application
     */
    protected function _sortRoutes()
    {
        $slashCount = array();
        if(null !== $this->_routes) {
            foreach ($this->_routes as $name => $routeInfo) {
                $chars = count(explode('/', $routeInfo['uri'])) - 1;
                $slashCount[$name] = $chars;
            }
            arsort($slashCount);
            $newRouteArray = array();
            foreach ($slashCount as $name => $count) {
                $newRouteArray[$name] = $this->_routes[$name];
            }
            $this->_routes = $newRouteArray;
        }
        return $this;
    }

    /**
     * Load default application routes
     * 
     * @return Aero_Controller_Application
     */
    protected function _loadDefaultRoutes()
    {
        $routes = array('moduleControllerAction' => array('uri'        => ':module/:controller/:action'),
                        'controllerAction'       => array('uri'        => ':controller/:action',
                                                          'module'     => 'default'),
                        'action'                 => array('uri'        => ':action',
                                                          'module'     => 'default',
                                                          'controller' => 'index'),
                        'blank'                  => array('uri'        => '' ,
                                                          'module'     => 'default',
                                                          'controller' => 'index',
                                                          'action'     => 'index'));
        return $this->registerRoutes($routes);
    }

    /**
     * Set the requested URI
     * 
     * @return Aero_Controller_Application
     */
    protected function _setRequestUri()
    {
        $uri = preg_replace('/' . str_replace('/', '\/', $this->_baseUrl) . '/', '', $_SERVER['REQUEST_URI'], 1);

        if ($uri != '' && $uri[strlen($uri) - 1] == '/') {
            $uri = substr($uri, 0, - 1);
        }
        $this->_requestUri = $uri;
        return $this;
    }

    /**
     * Get the request URI
     * 
     * @return string
     */
    public function getRequestUri()
    {
        return $this->_requestUri;
    }

    /**
     * Route the request
     * 
     * @return Aero_Controller_Application
     */
    protected function _route()
    {
        $routed = false;
        foreach ($this->_routes as $name => $routeInfo) {
            $route = $routeInfo['uri'];
            $exp = '/' . str_replace('/', '\\/', preg_replace('/:[^\/]+/', '[^/]+', $route)) . '/';
            if (preg_match($exp, $this->getRequestUri())) {
                $routeParts = explode('/', $routeInfo['uri']);
                $requestParts = explode('/', $this->getRequestUri());
                foreach ($routeParts as $key => $routePart) {
                    if (substr($routePart, 0, 1) == ':') {
                        $varName = ltrim($routePart, ':');
                        $params[$varName] = $requestParts[$key];
                    }
                }
                if (isset($params['module'])) {
                    $this->setRequestedModule($params['module']);
                    unset($params['module']);
                } elseif (isset($routeInfo['module'])) {
                    $this->setRequestedModule($routeInfo['module']);
                }
                if (isset($params['controller'])) {
                    $this->setRequestedController($params['controller']);
                    unset($params['controller']);
                } elseif (isset($routeInfo['controller'])) {
                    $this->setRequestedController($routeInfo['controller']);
                }
                if (isset($params['action'])) {
                    $this->setRequestedAction($params['action']);
                    unset($params['action']);
                } elseif (isset($routeInfo['action'])) {
                    $this->setRequestedAction($routeInfo['action']);
                }

                if(isset($params)) {
                    $this->_params = $params;
                }

                /**
                 * Load Defaults
                 */
                if($this->getRequestedModule() == '') {
                    $this->setRequestedModule($this->getDefaultModule());
                }

                if($this->getRequestedController() == '') {
                    $this->setRequestedController($this->getDefaultController());
                }

                if($this->getRequestedAction() == '') {
                    $this->setRequestedAction($this->getDefaultAction());
                }

                $routed = true;
                break;
            }
        }
        if (! $routed) {
            $this->setRequestedModule($this->getDefaultModule());
            $this->setRequestedController('error');
            $this->setRequestedAction('notFound');
        }
        return $this;
    }

    /**
     * Verify controller and action exist
     * 
     * @return bool
     */
    protected function _isDispatchable()
    {
        $dispatchable = false;
        $controllerFile = $this->_getControllerFile($this->_getControllerClass());
        /**
         * Controller File Exists
         */
        if (! file_exists($controllerFile)) {
            $this->setRequestedModule('default');
            $this->setRequestedController('error');
            $this->setRequestedAction('notFound');
            if ($this->getRequestedController() == 'error') {
                return false;
            }
            return $this->_isDispatchable();
        }
        /**
         * Action is callable
         */
        require_once $controllerFile;
        if (! method_exists($this->_getControllerClass(), $this->_getControllerAction())) {
            $this->setRequestedModule('default');
            $this->setRequestedController('error');
            $this->setRequestedAction('notFound');
            if ($this->getRequestedController() == 'error') {
                return false;
            }
            return $this->_isDispatchable();
        }
        return true;
    }

    /**
     * Get action name
     * 
     * @return string
     */
    protected function _getControllerAction()
    {
        return strtolower($this->getRequestedAction()) . 'Action';
    }

    /**
     * Get controller class name
     * 
     * @return string
     */
    protected function _getControllerClass()
    {
        return ucfirst(strtolower($this->getRequestedController())) . 'Controller';
    }

    /**
     * Get controller file name and path
     * 
     * @param  string $class
     * @return string
     */
    protected function _getControllerFile($class)
    {
        $controllerFile = $this->getModuleDirectory() . '/';
        $controllerFile .= $this->getRequestedModule() . '/';
        $controllerFile .= $this->getControllerDirectory() . '/';
        $controllerFile .= $class . '.php';
        return $controllerFile;
    }

    /**
     * Get controller class object
     * 
     * @return Aero_Controller_Page
     */
    protected function _getControllerObject()
    {
        $reflection = new ReflectionClass($this->_getControllerClass());
        return $reflection->newInstance();
    }

    /**
     * Run the dispatch process
     * 
     * @return string
     */
    public function dispatch()
    {
        // Order the routes
        $this->_sortRoutes();
        $this->_loadDefaultRoutes();
        $this->_route();
        if (! $this->_isDispatchable()) {
            Aero_Loader::loadClass('Aero_Controller_Exception');
            throw new Aero_Controller_Exception('Unable to dispatch request and unable to find an
                                                 ErrorController class in the default module with
                                                 a valid notFound method.');
        }

        $controller = $this->_getControllerObject();
    }
}